package stock

import (
	"context"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	"mall/common/cachex"
	"mall/common/resultx"
	"mall/service/backend/common/i18n"
	"mall/service/product/rpc/internal/svc"
	"mall/service/product/rpc/types/product"

	// "github.com/sirupsen/logrus"
	"github.com/zeromicro/go-zero/core/logx"
)

type StockItem struct {
	StockKey  string `json:"stock_key"`
	FrozenKey string `json:"frozen_key"`
	Quantity  string `json:"quantity"`
}

const (
	luaCheckAndSubtractStockScript = `
if (ARGV[1] and ARGV[1] ~= "") then
  local json = cjson.decode(ARGV[1])
  local breakFlag = false
  for k, v in pairs(json) do
    local total = tonumber(redis.call('GET', v.stock_key))
    local frozen = tonumber(redis.call('GET', v.frozen_key))
    local quantity = tonumber(v.quantity)
    if frozen + quantity > total then
      breakFlag = true
      break
    end
  end
  if breakFlag then
    return 0
  else
    for k, v in pairs(json) do
      local quantity = tonumber(v.quantity)
      redis.call('INCRBY', v.frozen_key, quantity)
    end
  end
  return 1
end
return 0
`
)

type CheckAndSubtractStockLogic struct {
	ctx    context.Context
	svcCtx *svc.ServiceContext
	logx.Logger
}

func NewCheckAndSubtractStockLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckAndSubtractStockLogic {
	return &CheckAndSubtractStockLogic{
		ctx:    ctx,
		svcCtx: svcCtx,
		Logger: logx.WithContext(ctx),
	}
}

func (l *CheckAndSubtractStockLogic) CheckAndSubtractStock(
	in *product.CheckAndSubtractStockReq,
) (*product.BaseMsg, error) {
	// logrus.Info(fmt.Sprintf("CheckAndSubtractStockReq : %+v ", in))

	if len(in.Data) > 0 {
		if in.CheckOnly != nil && *in.CheckOnly > 0 {
			return l.checkStock(in.Data)
		}

		return l.checkAndSubtractStock(in.Data)
	}

	return &product.BaseMsg{}, nil
}

func (l *CheckAndSubtractStockLogic) checkStock(
	data []*product.ProductVariationStock,
) (*product.BaseMsg, error) {
	var keys []string
	idxStockMap := make(map[int]uint32)
	idxIdMap := make(map[int]string)
	for idx, v := range data {
		keys = append(keys, getStockKey(v.Id))
		keys = append(keys, getStockFrozenKey(v.Id))

		idxStockMap[idx+1] = v.Stock
		idxIdMap[idx+1] = strconv.FormatUint(v.Id, 10)
	}

	vals, err := l.svcCtx.BizRedis.MgetCtx(l.ctx, keys...)
	if err != nil {
		// logrus.Info(fmt.Sprintf("CheckAndSubtractStock checkStock err : %+v ", err))
		logx.Errorw("CheckAndSubtractStock checkStock err", logx.Field("detail", err.Error()))
		return nil, err
	}

	var stock uint32
	var frozen uint32
	var vids []string
	for idx, val := range vals {
		if idx%2 == 1 { // frozen
			if len(val) > 0 { // redis 有记录
				if i, err := strconv.ParseInt(val, 10, 64); err == nil && i >= 0 {
					frozen = uint32(i)
				}
			}
		} else { // stock
			if len(val) > 0 { // redis 有记录
				if i, err := strconv.ParseInt(val, 10, 64); err == nil && i >= 0 {
					stock = uint32(i)
				}
			}
		}

		if idx%2 == 1 {
			if qty, ok := idxStockMap[(idx+1)/2]; ok {
				if frozen+qty > stock {
					if id, ok := idxIdMap[(idx+1)/2]; ok {
						vids = append(vids, id)
					} else {
						vids = append(vids, "0") // Todo: 似乎无意义
					}
				}
			}
			stock = 0
			frozen = 0
		}
	}

	if len(vids) == 0 {
		return &product.BaseMsg{}, nil
	}

	return &product.BaseMsg{
		Msg: strings.Join(vids, ","),
	}, nil
}

func (l *CheckAndSubtractStockLogic) checkAndSubtractStock(
	data []*product.ProductVariationStock,
) (*product.BaseMsg, error) {
	var items []*StockItem
	var keys []string // 脚本里使用的所有键都应该由 KEYS 数组来传递
	// https://www.jianshu.com/p/15568d7fc275
	// 所有的 Redis 命令，在执行之前都会被分析，籍此来确定命令会对哪些键进行操作。必须使用正确的形式来传递键，才能确保分析工作正确地执行。
	// 除此之外，使用正确的形式来传递键还有很多其他好处，它的一个特别重要的用途就是确保 Redis 集群可以将你的请求发送到正确的集群节点。
	for _, v := range data {
		stockKey := getStockKey(v.Id)
		frozenKey := getStockFrozenKey(v.Id)
		items = append(items, &StockItem{
			StockKey:  stockKey,
			FrozenKey: frozenKey,
			Quantity:  strconv.FormatInt(int64(v.Stock), 10),
		})
		keys = append(keys, stockKey)
		keys = append(keys, frozenKey)
	}

	payload, err := json.Marshal(items)
	if err != nil {
		// logrus.Info(fmt.Sprintf("CheckAndSubtractStock json.Marshal err : %+v ", err))
		logx.Errorw("CheckAndSubtractStock json.Marshal err", logx.Field("detail", err.Error()))
		return nil, resultx.StatusError(resultx.PRODUCT_VARIATION_STOCK_VALIDATE_FAIL, "")
	}

	// 用KEYS传值，否则报错
	val, err := l.svcCtx.BizRedis.EvalCtx(l.ctx, luaCheckAndSubtractStockScript, keys, payload)
	if err != nil {
		// logrus.Info(fmt.Sprintf("CheckAndSubtractStock err : %+v ", err))
		logx.Errorw("CheckAndSubtractStock err", logx.Field("detail", err.Error()))
		// @user_script:16: @user_script: 16: Lua script attempted to access a non local key in a cluster node
		// CROSSSLOT Keys in request don't hash to the same slot
		return nil, resultx.StatusError(resultx.PRODUCT_VARIATION_STOCK_INVALID, "")
	}

	if i, ok := val.(int64); ok && i == 1 {
		return &product.BaseMsg{
			Msg: i18n.UpdateSuccess,
		}, nil
	}

	return nil, resultx.StatusError(resultx.PRODUCT_VARIATION_STOCK_INVALID, "")
}

func getStockKey(vid uint64) string {
	return fmt.Sprintf("{%s}:%d", cachex.SkuStock, vid)
}

func getStockFrozenKey(vid uint64) string {
	return fmt.Sprintf("{%s}:%d:%s", cachex.SkuStock, vid, cachex.SkuStockFrozen)
}
